﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using AmiEmuLib.Native;
using log4net;
using TomanuExtensions;

namespace AmiEmuLib.Host.HostInput
{
    /// <summary>
    /// Listener for events.
    /// </summary>
    /// <param name="a_event">
    /// Generated event.
    /// </param>
    /// <param name="a_pushed">
    /// Depends on events. Use only for buttons and keys.
    /// </param>
    /// <param name="a_value">
    /// Depends on events. It can be absolute or relative mouse move, joystick analog axle,
    /// virtual key code correspoding to event id.
    /// </param>
    public delegate void InputEvent(HostEvent a_event, bool a_pushed, int a_value);

    /// <summary>
    /// Manage all input devices.
    /// </summary>
    public static class HostInputDevicesList
    {
        /// <summary>
        /// Logger.
        /// </summary>
        private static readonly ILog Logger =
            LogManager.GetLogger(typeof(HostInputDevicesList).GetParentFullName());

        /// <summary>
        /// Control Initialize().
        /// </summary>
        private static bool m_was_initialize = false;

        /// <summary>
        /// Input devices groups.
        /// </summary>
        private static readonly List<HostInputDevices> m_list = new List<HostInputDevices>();

        /// <summary>
        /// Represent keyboards group.
        /// </summary>
        public static readonly HostKeyboardsDevices Keyboards = new HostKeyboardsDevices();

        /// <summary>
        /// Represent mice group.
        /// </summary>
        public static readonly HostMiceDevices Mice = new HostMiceDevices();

        /// <summary>
        /// Generated on every event. No information about how value was changed,
        /// fit for GUI events mapping. Some events can be filtered is HiddenFromMapping
        /// was specified.
        /// </summary>
        public static event InputEvent OnEvent;

        /// <summary>
        /// Static constructor.
        /// </summary>
        static HostInputDevicesList()
        {
            m_list.Add(Keyboards);
            m_list.Add(Mice);
        }

        /// <summary>
        /// Call once before first use any input device. Called from HostEnvironment.
        /// </summary>
        internal static void Connect()
        {
            if (m_was_initialize)
            {
                Logger.Error("Circular call for Initialize()");
                return;
            }

            m_was_initialize = true;

            Rescan();

            m_list.ForEach(d => d.Connect());

            HostEnvironment.Input += ProcessRawInputMessage;
            m_list.ForEach(hids => hids.OnEvent += DoEvent);
            HostEnvironment.HostWindowDetached += Disconnect;
            HostEnvironment.DeviceChanged += Rescan;
        }

        private static void Disconnect()
        {
            m_list.ForEach(hids => hids.OnEvent -= DoEvent);
            HostEnvironment.HostWindowDetached -= Disconnect;
            HostEnvironment.Input -= ProcessRawInputMessage;
            HostEnvironment.DeviceChanged -= Rescan;
            m_list.ForEach(d => d.Disconnect());
            m_was_initialize = false;
        }

        /// <summary>
        /// Send events from devices groups outside, most common to GUI when mapping.
        /// </summary>
        /// <param name="a_event"></param>
        /// <param name="a_pushed"></param>
        /// <param name="a_value"></param>
        private static void DoEvent(HostEvent a_event, bool a_pushed, int a_value)
        {
            if (a_event.IgnoreWhenMapping)
                return;
            if (OnEvent != null)
                OnEvent(a_event, a_pushed, a_value);
        }

        /// <summary>
        /// Is device name represent remote desktop device.
        /// </summary>
        /// <param name="a_name"></param>
        /// <returns></returns>
        private static bool IsRemoteDesktopDevice(string a_name)
        {
            return a_name.IndexOf("\\\\?\\Root#") == 0;
        }

        /// <summary>
        /// Rescan all devices.
        /// </summary>
        internal static void Rescan()
        {
            uint count = 0;
            NativeRawInput.GetRawInputDeviceList(null, ref count,
                NativeRawInput.RawInputDeviceList.StructSize);
            NativeRawInput.RawInputDeviceList[] list = new NativeRawInput.RawInputDeviceList[count];
            NativeRawInput.GetRawInputDeviceList(list, ref count,
                NativeRawInput.RawInputDeviceList.StructSize);

            m_list.ForEach(d => d.ClearDevices());

            for (int i = 0; i < count; i++)
            {
                uint size = 0;
                NativeRawInput.GetRawInputDeviceInfo(list[i].hDevice,
                    NativeRawInput.RawInputDeviceInfoCommand.DEVICENAME, IntPtr.Zero, ref size);

                StringBuilder str_builder = new StringBuilder((int)size);
                NativeRawInput.GetRawInputDeviceInfo(list[i].hDevice,
                    NativeRawInput.RawInputDeviceInfoCommand.DEVICENAME, str_builder, ref size);
                string device_name = str_builder.ToString();

                if (IsRemoteDesktopDevice(device_name))
                    continue;

                NativeRawInput.RawInputDeviceInfo rdi = new NativeRawInput.RawInputDeviceInfo();
                rdi.Size = Marshal.SizeOf(typeof(NativeRawInput.RawInputDeviceInfo));
                size = (uint)rdi.Size;
                NativeRawInput.GetRawInputDeviceInfo(list[i].hDevice,
                    NativeRawInput.RawInputDeviceInfoCommand.DEVICEINFO, ref rdi, ref size);

                if (list[i].Type == NativeRawInput.RawInputDeviceType.KEYBOARD)
                {
                    HostKeyboardDevice keyboard = new HostKeyboardDevice();
                    keyboard.DeviceName = device_name;

                    Keyboards.AddDevice(keyboard);
                }
                else if (list[i].Type == NativeRawInput.RawInputDeviceType.MOUSE)
                {
                    HostMouseDevice mouse = new HostMouseDevice();
                    mouse.Buttons = rdi.Mouse.NumberOfButtons;
                    mouse.DeviceName = device_name;

                    Mice.AddDevice(mouse);
                }
            }

            GetFriendlyNames();

            m_list.ForEach(d => d.Changed());

            Logger.Info("Connected mouses:");
            foreach (var device in Mice.Devices)
                Logger.InfoFormat("Name: {0}, buttons: {1}", device.FriendlyName, device.Buttons);

            Logger.Info("Connected keyboards:");
            foreach (var device in Keyboards.Devices)
                Logger.InfoFormat("Name: {0}", device.FriendlyName);
        }

        /// <summary>
        /// Find friendly name for input device hardware identificator.
        /// </summary>
        private static void GetFriendlyNames()
        {
            Guid[] guids = new[] { NativeConsts.GUID_DEVCLASS_MOUSE,
                NativeConsts.GUID_DEVCLASS_KEYBOARD };

            foreach (var guid in guids)
            {
                Guid g = guid;
                IntPtr di = NativeSetupDevice.SetupDiGetClassDevs(ref g, null, IntPtr.Zero,
                    NativeSetupDevice.SetupDiGetClassDevsFlags.DIGCF_PRESENT);

                if (di == NativeConsts.INVALID_HANDLE_VALUE)
                {
                    int error = Marshal.GetLastWin32Error();
                    Logger.ErrorFormat("SetupDiGetClassDevs() failed (error: {0})", error);
                    return;
                }

                NativeSetupDevice.SetupDeviceInfoData dd = new NativeSetupDevice.SetupDeviceInfoData();
                dd.Size = NativeSetupDevice.SetupDeviceInfoData.StructSize;

                for (uint i = 0; ; i++)
                {
                    if (!NativeSetupDevice.SetupDiEnumDeviceInfo(di, i, ref dd))
                    {
                        int error = Marshal.GetLastWin32Error();
                        if (error != NativeConsts.ERROR_NO_MORE_ITEMS)
                            Logger.ErrorFormat("SetupDiEnumDeviceInfo() failed (error: {0})", error);
                        break;
                    }

                    uint size;
                    if (!NativeSetupDevice.SetupDiGetDeviceInstanceId(
                        di, ref dd, IntPtr.Zero, 0, out size))
                    {
                        int error = Marshal.GetLastWin32Error();
                        if (error != NativeConsts.ERROR_INSUFFICIENT_BUFFER)
                        {
                            Logger.ErrorFormat("SetupDiGetDeviceInstanceId() failed #1 (error: {0})",
                                error);
                            break;
                        }
                    }
                    StringBuilder device_name_buffer = new StringBuilder((int)size);
                    if (!NativeSetupDevice.SetupDiGetDeviceInstanceId(
                        di, ref dd, device_name_buffer, size, out size))
                    {
                        int error = Marshal.GetLastWin32Error();
                        Logger.ErrorFormat("SetupDiGetDeviceInstanceId() failed #2 (error: {0})",
                            error);
                        break;
                    }

                    uint reg_type;
                    size = 0;
                    if (!NativeSetupDevice.SetupDiGetDeviceRegistryProperty(di, ref dd,
                            NativeSetupDevice.SetupDiGetDeviceRegistryPropertyEnum.SPDRP_FRIENDLYNAME,
                                out reg_type, IntPtr.Zero, 0, out size))
                    {
                        int error = Marshal.GetLastWin32Error();
                        if ((error != NativeConsts.ERROR_INVALID_DATA) &&
                            (error != NativeConsts.ERROR_INSUFFICIENT_BUFFER))
                        {
                            Logger.ErrorFormat("SetupDiGetDeviceRegistryProperty() failed #1 (error: {0})",
                                error);
                            break;
                        }
                    }

                    StringBuilder friendly_name;

                    if (size == 0)
                    {
                        if (!NativeSetupDevice.SetupDiGetDeviceRegistryProperty(di, ref dd,
                                NativeSetupDevice.SetupDiGetDeviceRegistryPropertyEnum.SPDRP_DEVICEDESC,
                                    out reg_type, IntPtr.Zero, 0, out size))
                        {
                            int error = Marshal.GetLastWin32Error();
                            if ((error != NativeConsts.ERROR_INVALID_DATA) &&
                                (error != NativeConsts.ERROR_INSUFFICIENT_BUFFER))
                            {
                                Logger.ErrorFormat("SetupDiGetDeviceRegistryProperty() failed #2 (error: {0})",
                                    error);
                                break;
                            }
                        }
                        if (size != 0)
                        {
                            friendly_name = new StringBuilder((int)size);
                            if (!NativeSetupDevice.SetupDiGetDeviceRegistryProperty(di, ref dd,
                                     NativeSetupDevice.SetupDiGetDeviceRegistryPropertyEnum.SPDRP_DEVICEDESC,
                                        out reg_type, friendly_name, size, out size))
                            {
                                int error = Marshal.GetLastWin32Error();
                                Logger.ErrorFormat("SetupDiGetDeviceRegistryProperty() failed #3 (error: {0})",
                                    error);
                                break;
                            }
                        }
                        else
                        {
                            Logger.Info("Cannot find friendly name.");
                            continue;
                        }
                    }
                    else
                    {
                        friendly_name = new StringBuilder((int)size);
                        if (!NativeSetupDevice.SetupDiGetDeviceRegistryProperty(di, ref dd,
                                NativeSetupDevice.SetupDiGetDeviceRegistryPropertyEnum.SPDRP_FRIENDLYNAME,
                                    out reg_type, friendly_name, size, out size))
                        {
                            int error = Marshal.GetLastWin32Error();
                            Logger.ErrorFormat("SetupDiGetDeviceRegistryProperty() failed #4 (error: {0})",
                                error);
                            break;
                        }
                    }

                    string device_name = "\\\\?\\" +
                        device_name_buffer.ToString().Replace("\\", "#").ToUpper();

                    foreach (var device in
                        Keyboards.Devices.Cast<HostInputDevice>().Concat(
                            Mice.Devices.Cast<HostInputDevice>()))
                    {
                        if (device.DeviceName.ToUpper().StartsWith(device_name))
                        {
                            device.FriendlyName = friendly_name.ToString();
                            break;
                        }
                    }
                }

                if (!NativeSetupDevice.SetupDiDestroyDeviceInfoList(di))
                {
                    int error = Marshal.GetLastWin32Error();
                    Logger.ErrorFormat("SetupDiDestroyDeviceInfoList() failed (error: {0})",
                        error);
                }
            }
        }

        /// <summary>
        /// Called from main window message loop. Process raw input events.
        /// </summary>
        /// <param name="a_msg"></param>
        private static void ProcessRawInputMessage(ref Message a_msg)
        {
            NativeRawInput.RawInput ri;
            uint size = NativeRawInput.RawInput.StructSize;
            if (NativeRawInput.GetRawInputData(a_msg.LParam, NativeRawInput.RawInputCommand.Input,
                    out ri, ref size, NativeRawInput.RawInputHeader.StructSize) == uint.MaxValue)
            {
                Logger.Error("GetRawInputData() failed");
                return;
            }

            if (ri.Header.Type == NativeRawInput.RawInputDeviceType.KEYBOARD)
            {
                Keyboards.ProcessRawInputMessage(
                    a_msg.WParam == NativeRawInput.RIM_INPUT, ri);
            }
            else
            {
                Mice.ProcessRawInputMessage(
                    a_msg.WParam == NativeRawInput.RIM_INPUT, ri);
            }
        }

        /// <summary>
        /// Listen for this event.
        /// </summary>
        /// <param name="a_event"></param>
        /// <param name="a_listener"></param>
        public static void RegisterEvent(HostEvent a_event, InputEvent a_listener)
        {
            foreach (var d in m_list)
            {
                if (d.RegisterEvent(a_event, a_listener))
                    return;
            }

            Logger.ErrorFormat("RegisterEvent() failed, event: {0}", a_event.Name);

            throw new InvalidOperationException();
        }

        /// <summary>
        /// Stop listening for this event.
        /// </summary>
        /// <param name="a_event"></param>
        /// <param name="a_listener"></param>
        public static void UnregisterEvent(HostEvent a_event, InputEvent a_listener)
        {
            foreach (var d in m_list)
            {
                if (d.UnregisterEvent(a_event, a_listener))
                    return;
            }

            Logger.ErrorFormat("UnregisterEvent() failed, event: {0}", a_event.Name);

            throw new InvalidOperationException();
        }

        /// <summary>
        /// Input devices groups.
        /// </summary>
        public static IEnumerable<HostInputDevices> List
        {
            get
            {
                return m_list;
            }
        }
    }
}